package upbeat.jmusiclib;

import upbeat.types.Measure;
import java.util.LinkedList;
import java.util.HashMap;
import java.util.Map;
import jm.music.data.*;
import jm.music.tools.Mod;
import jm.util.*;

import java.io.File;

/**
 * This is a wrapper for the JMusic library. It accepts a {@link Measure} of music at a time,
 * and uses JMusic to build up a midi file which is then written to disk. Currently, it 
 * writes all output to "upbeat.mid".
 * 
 * @author Matt
 *
 */
public class JMusicWrapper implements JMusicWrapperInterface{
	
	/**
	 * A JMusic datatype used to build up and then output the music.
	 */
	private static Score mScore = new Score();
	private static Map <String, Score> mScores = new HashMap<String, Score>();
	private static int mCurrentMeasure = 1;
	private static int mTempo = 100;
	
	/**
	 * Used by the Manager class to pass a new measure of music to JMusic.  At the moment, this should
	 * just be appended onto the end of a MIDI file.  Eventually, we'll want to add in another method
	 * for actually playing this stuff in real time.
	 * 
	 * @param newMeasure	This is a linked list containing a bunch of measures.  The measures of music
	 * in newMeasure should be played <i>simultaneously</i>.  Thus, even though there may be many {@link Measure}
	 * objects passed to AppendNextMeasure, they only actually produce one measure of music in the final song.
	 */
	public void AppendNextMeasure( LinkedList< Measure > newMeasure ) {
		
		// 1. step through list of measures
		// 2. extract appropriate measure from given filename
		// 3. build up JMusic Phrase
		// 4. append to mScore
		// 5. when finished, write to disk
		
		int timesig_num = 4;
		int timesig_den = 4;
		
		for (Measure measure : newMeasure) {
			
			// if this is a REST measure, add in silence and continue
			if (measure.getMidiName() == "REST") {
				continue;
			}

			Part newPart = new Part();
			
			/* 
			 * if this is the first time encountering this MIDI file,
			 * initialize it and add it to the map
			 */
			if (!mScores.containsKey(measure.getMidiName())) {
				
				// Parse in the MIDI file
				Score score = new Score(measure.toString());
				Read.midi(score, measure.getMidiName());
				
				// make sure there is valid data
				if (score.getPart(0) == null) {
					throw new Error("Bad MIDI file passed in: " + measure.toString());
				}
				
				// add the Score to our map
				mScores.put(measure.getMidiName(), score);
				
			}		
			
			Score score = mScores.get(measure.getMidiName());
			Part oldPart = score.getPart(0);
			
			// filter out phrases to the relevant measure
			// for (int i = 0; i < oldPart.getPhraseArray().length; i++) {
			for (Phrase p : oldPart.getPhraseArray()) {
				
				// ends too early, ignore
				if (p.getEndTime() < timesig_den * (measure.getMeasure() - 1)) {
					continue;
			    // starts too late, we can stop looking
				} else if (p.getStartTime() > timesig_den * measure.getMeasure()) {
					continue;
				}

				// add all valid notes from the phrase
				Phrase newPhrase = new Phrase();
				jm.music.data.Note[] notes = p.getNoteArray();
				double duration = p.getStartTime();
				int note = 0;
				
				for (; note < notes.length && duration < (measure.getMeasure() - 1) * timesig_den; note++) {
					duration += notes[note].getRhythmValue();
				}
				
				boolean firstNote = true;
				double firstTime = 0;
				
				for (; note < notes.length && duration < measure.getMeasure() * timesig_den; note++) {
					if (firstNote) {
						firstTime = duration;
						firstNote = false;
					}
					newPhrase.addNote(notes[note].copy());
					duration += notes[note].getRhythmValue();
				}
				

				double offset = firstTime;
				while (offset >= timesig_den) {
					offset -= timesig_den;
				}
				
				double startTime = ((mCurrentMeasure - 1) * timesig_den) + offset;
				newPhrase.setStartTime(startTime);
				//newPhrase.setStartTime(firstTime);
				
				newPart.add(newPhrase);
				
			}
			
			/* assign a channel to each part
			 * keep drums on MIDI channel 10 (9 in JMusic)
			 * use assigned channel for everything else
			 */
			if(oldPart.getChannel() == 9) {
				newPart.setChannel(9);
			} else {
				newPart.setChannel(measure.getChannel());
			}
			
			// set MIDI program (e.g. 1 is Acoustic Piano)
			newPart.setInstrument(measure.getInstrument());
			
			// perform potential pitch transposition
			Mod.transpose(newPart, measure.getPitch());
			
			// set tempo for the newly added part
			newPart.setTempo(mTempo);
			
			mScore.add(newPart);
			
		}
		
		mCurrentMeasure++;
		
	}
	
	/**
	 * Plays the song that has been built up through JMusic
	 */
	public void play() {
		Play.midi(mScore);
	}
		
	
	/**
	 * Return the length of an input file in measures
	 * @param fileName	The name of the file to count measures in
	 */
	public int length(String fileName) {
		Score s = new Score(fileName);
		Read.midi(s, fileName);
		double last = s.getEndTime() / 4.0;
		int length = (int)Math.round(last);
		
		if (s.getPart(0) == null) {
			throw new Error("Bad MIDI file passed in: " + fileName);
		}
		
		return length;
		
	}
	
	/**
	 * Sets the tempo for generated music
	 * @param tempo  The tempo to generate/play the music at.
	 */
	public void setTempo(int tempo) {
		mTempo = tempo;
	}
	
}
